package com.myjpa.common.criteria;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;
import com.myjpa.common.date.DateUtils;
import com.myjpa.common.web.PagedResponse;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.*;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author: yaoll
 * @date: 2020-09-04
 * @verison: 1.0
 */
public class CriteriaUtils {

	private static final Integer DEFAULT_SIZE = 50;

	private JPAQueryFactory queryFactory;

	public CriteriaUtils(JPAQueryFactory queryFactory) {
		this.queryFactory = queryFactory;
	}

	public <T, Q extends EntityPathBase<T>> PagedResponse<T> query(Q q, CriteriaExpressionParser parser, JacksonCriteria criteria) {
		PagedResponse<T> resp = new PagedResponse<T>();

		// 解析查询条件
		List<BooleanExpression> expressions = new LinkedList<>();
		if (parser != null) {
			List<BooleanExpression> parseExps = parser.parse(criteria);
			if (parseExps != null && parseExps.size() > 0) {
				expressions.addAll(parseExps);
			}
		}
		List<BooleanExpression> exps = parseCriteria(q, criteria);
		if (exps != null && exps.size() > 0) {
			expressions.addAll(exps);
		}

		JPAQuery<T> query = queryFactory.selectFrom(q).where(expressions.toArray(new BooleanExpression[0]));

		// 统计总数
		long count = query.fetchCount();
		resp.setCount(count);

		// 排序
		OrderSpecifier[] specifiers = parseOrders(q, criteria);
		if (specifiers != null) {
			query = query.orderBy(specifiers);
		}

		// 分页
		Integer page = criteria.getPage();
		Integer size = criteria.getSize();
		if (page != null) {
			if (size == null) {
				size = DEFAULT_SIZE; // 默认每页50
			}
			int offset = (page - 1) * size;
			if (offset < 0) {
				offset = 0;
			}
			query = query.offset(offset).limit(size);
		}
		List<T> data = query.fetch();
		resp.setData(data);
		return resp;
	}

	private <T, Q extends EntityPathBase<T>> OrderSpecifier[] parseOrders(Q q, JacksonCriteria criteria) {
		List<JacksonCriteria.Order> orders = criteria.getOrders();
		if (orders == null || orders.size() == 0) {
			return null;
		}
		List<OrderSpecifier> specifiers = new LinkedList<>();
		Field[] fields = q.getClass().getFields();
		for (JacksonCriteria.Order order : orders) {
			String column = order.getColumn();
			try {
				for (Field field : fields) {
					String name = field.getName();
					if (!column.equals(name)) {
						continue;
					}
					Class<?> type = field.getType();
					if (type == NumberPath.class) {
						NumberPath o = (NumberPath) field.get(q);
						specifiers.add(order.isAsc() ? o.asc() : o.desc());
					}
					if (type == DateTimePath.class) {
						DateTimePath o = (DateTimePath) field.get(q);
						specifiers.add(order.isAsc() ? o.asc() : o.desc());
					}
					if (type == StringPath.class) {
						StringPath o = (StringPath) field.get(q);
						specifiers.add(order.isAsc() ? o.asc() : o.desc());
					}
				}
			} catch (IllegalAccessException e) {

			}
		}

		if (specifiers.size() == 0) {
			return null;
		} else {
			return specifiers.toArray(new OrderSpecifier[0]);
		}
	}

	private <T, Q extends EntityPathBase<T>> List<BooleanExpression> parseCriteria(Q q, JacksonCriteria criteria) {
		List<BooleanExpression> exps = new LinkedList<>();
		ObjectNode object = criteria.getCriteria();
		if (object == null || object.isEmpty()) {
			return exps;
		}

		Field[] fields = q.getClass().getFields();
		for (Field field : fields) {
			Class<?> type = field.getType();
			if (type != NumberPath.class && type != DateTimePath.class && type != StringPath.class) {
				continue;
			}
			String name = field.getName();
			if (!object.has(name)) {
				continue;
			}
			JsonNode jsonNode = object.get(name);
			try {
				if (type == NumberPath.class) {
					NumberPath path = (NumberPath) field.get(q);
					exps.addAll(parse(path, jsonNode));
				}
				if (type == DateTimePath.class) {
					DateTimePath path = (DateTimePath) field.get(q);
					exps.addAll(parse(path, jsonNode));
				}
				if (type == StringPath.class) {
					StringPath path = (StringPath) field.get(q);
					exps.addAll(parse(path, jsonNode));
				}
				// Boolean Enum
			} catch (IllegalAccessException e) {
			}
		}
		return exps;
	}

	public static List<BooleanExpression> parse(NumberPath path, JsonNode node) {
		return parse(path, node, null);
	}

	public static List<BooleanExpression> parse(NumberPath path, JsonNode node, Integer defaultValue) {
		List<BooleanExpression> result = new LinkedList<>();
		if (node == null) {
			if (defaultValue != null) {
				result.add(path.eq(defaultValue));
			}
			return result;
		}
		JsonNodeType nodeType = node.getNodeType();
		if (nodeType == JsonNodeType.STRING) {
			result.add(path.eq(parseNumber(node)));
		} else if (nodeType == JsonNodeType.NUMBER) {
			result.add(path.eq(parseNumber(node)));
		} else if (nodeType == JsonNodeType.ARRAY) {
			ArrayNode arr = (ArrayNode) node;
			result.add(path.in(parseNumberArr(arr)));
		} else if (nodeType == JsonNodeType.OBJECT) {
			if (node.has("$gt")) {
				result.add(path.gt(parseNumber(node.get("$gt"))));
			}
			if (node.has("$gte")) {
				result.add(path.goe(parseNumber(node.get("$gte"))));
			}
			if (node.has("$lt")) {
				result.add(path.lt(parseNumber(node.get("$lt"))));
			}
			if (node.has("$lte")) {
				result.add(path.loe(parseNumber(node.get("$lte"))));
			}
			if (node.has("$in")) {
				ArrayNode arr = (ArrayNode) node.get("$in");
				result.add(path.in(parseNumberArr(arr)));
			}
			if (node.has("$ne")) {
				result.add(path.ne(parseNumber(node.get("$ne"))));
			}
			if (node.has("$null")) {
				Boolean isNull = Boolean.valueOf(node.get("$null").asText());
				if (isNull) {
					result.add(path.isNull());
				} else {
					result.add(path.isNotNull());
				}
			}
		}
		return result;
	}

	private static Number parseNumber(JsonNode node) {
		if (node instanceof IntNode) {
			return node.asInt();
		} else if (node instanceof LongNode) {
			return node.asLong();
		} else if (node instanceof DoubleNode) {
			return node.doubleValue();
		} else if (node instanceof TextNode) {
			String n = node.asText();
			if (n.indexOf(".") > -1) {
				return node.asDouble();
			} else {
				return node.asLong();
			}
		}
		return 0;
	}

	public static List<Number> parseNumberArr(ArrayNode arr) {
		List<Number> values = new ArrayList<>(arr.size());
		Iterator<JsonNode> elements = arr.elements();
		while (elements.hasNext()) {
			values.add(parseNumber(elements.next()));
		}
		return values;
	}

	public static List<BooleanExpression> parse(StringPath path, JsonNode node) {
		return parse(path, node, null);
	}

	public static List<BooleanExpression> parse(StringPath path, JsonNode node, String defaultValue) {
		List<BooleanExpression> result = new LinkedList<>();
		if (node == null) {
			if (defaultValue != null) {
				result.add(path.eq(defaultValue));
			}
			return result;
		}
		JsonNodeType nodeType = node.getNodeType();
		if (nodeType == JsonNodeType.STRING) {
			result.add(path.eq(node.asText()));
		} else if (nodeType == JsonNodeType.ARRAY) {
			ArrayNode arr = (ArrayNode) node;
			result.add(path.in(parseStringArr(arr)));
		} else if (nodeType == JsonNodeType.OBJECT) {
			if (node.has("$gt")) {
				result.add(path.gt(node.get("$gt").asText()));
			}
			if (node.has("$gte")) {
				result.add(path.goe(node.get("$gte").asText()));
			}
			if (node.has("$lt")) {
				result.add(path.lt(node.get("$lt").asText()));
			}
			if (node.has("$lte")) {
				result.add(path.loe(node.get("$lte").asText()));
			}
			if (node.has("$in")) {
				ArrayNode arr = (ArrayNode) node.get("$in");
				result.add(path.in(parseStringArr(arr)));
			}
			if (node.has("$ne")) {
				result.add(path.ne(node.get("$ne").asText()));
			}
			if (node.has("$like")) {
				result.add(path.like(node.get("$like").asText()));
			}
			if (node.has("$notLike")) {
				result.add(path.notLike(node.get("$notLike").asText()));
			}
			if (node.has("$null")) {
				Boolean isNull = Boolean.valueOf(node.get("$null").asText());
				if (isNull) {
					result.add(path.isNull());
				} else {
					result.add(path.isNotNull());
				}
			}
		}
		return result;
	}

	private static List<String> parseStringArr(ArrayNode arr) {
		List<String> values = new ArrayList<>(arr.size());
		Iterator<JsonNode> elements = arr.elements();
		while (elements.hasNext()) {
			values.add(elements.next().asText());
		}
		return values;
	}

	public static List<BooleanExpression> parse(DateTimePath path, JsonNode node) {
		return parse(path, node, (Date) null);
	}

	/**
	 * @param path
	 * @param node
	 * @param time yyyyMMddHHmmss
	 * @return
	 */
	public static List<BooleanExpression> parse(DateTimePath path, JsonNode node, String time) {
		if (time != null) {
			return parse(path, node, DateUtils.parse(time));
		} else {
			return parse(path, node, (Date) null);
		}
	}

	public static List<BooleanExpression> parse(DateTimePath path, JsonNode node, Date time) {
		List<BooleanExpression> result = new LinkedList<>();
		if (node == null) {
			if (time != null) {
				result.add(path.eq(time));
			}
			return result;
		}
		SimpleDateFormat format = new SimpleDateFormat(DateUtils.PATTERN_yyyyMMddHHmmss);
		JsonNodeType nodeType = node.getNodeType();
		if (nodeType == JsonNodeType.STRING) {
			try {
				result.add(path.eq(format.parse(node.asText())));
			} catch (ParseException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		} else if (nodeType == JsonNodeType.OBJECT) {
			if (node.has("$gt")) {
				result.add(path.gt(parse(format, node, "$gt")));
			}
			if (node.has("$gte")) {
				result.add(path.goe(parse(format, node, "$gte")));
			}
			if (node.has("$lt")) {
				result.add(path.lt(parse(format, node, "$lt")));
			}
			if (node.has("$lte")) {
				result.add(path.loe(parse(format, node, "$lte")));
			}
			if (node.has("$null")) {
				Boolean isNull = Boolean.valueOf(node.get("$null").asText());
				if (isNull) {
					result.add(path.isNull());
				} else {
					result.add(path.isNotNull());
				}
			}
		}
		return result;
	}

	private static Date parse(SimpleDateFormat format, JsonNode node, String name) {
		try {
			return format.parse(node.get(name).asText());
		} catch (ParseException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

}
